#!/bin/sh
#-------------------------------------------------------------------------+
# Copyright (C) 2016 Matt Churchyard (churchers@gmail.com)
# All rights reserved
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted providing that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# creaate bridge interface for a standard switch
#
# @param string _name name of the switch
#
switch::standard::init(){
    local _name="$1"
    local _id _addr _mtu _len _ifconf

    # see if it already exists
    switch::standard::id "_id" "${_name}" && return 0

    # get the length of the switch name
    # it's useful for other utilities to use switch name as interface name
    # as it's static. can't do that if it's > 12 chars
    _len=$(echo -n "${_name}" | wc -m)

    if [ ${_len} -le 12 ]; then
      _ifconf="name vm-${_name}"
    else
      _ifconf="descr vm/${_name}"
    fi

    # create a bridge for this switch
    _id=$(ifconfig bridge create ${_ifconf} group vm-switch up 2>/dev/null)
    [ $? -eq 0 ] || util::err "failed to create bridge interface for switch ${_name}"

    switch::set_viid "${_name}" "${_id}"

    # randomise mac if feature is available
    [ ${VERSION_BSD} -ge 1102000 ] && ifconfig "${_id}" link random

    # try to set ip address
    config::core::get "_addr" "addr_${_name}"
    [ -n "${_addr}" ] && ifconfig "${_id}" inet ${_addr} 2>/dev/null
    config::core::get "_addr" "addr6_${_name}"
    [ -n "${_addr}" ] && ifconfig "${_id}" inet6 ${_addr} 2>/dev/null

    # custom mtu?
    config::core::get "_mtu" "mtu_${_name}"
    [ -n "${_mtu}" ] && ifconfig "${_id}" mtu ${_mtu}

    # add member interfaces
    switch::standard::__add_members "${_name}" "${_id}"
}

# show the configuration details for a switch
#
# @param string _name the switch name
# @param string _format output format
#
switch::standard::show(){
    local _name="$1"
    local _format="$2"
    local _id _vlan _ports _addr _mtu _priv

    switch::find "_id" "${_name}"
    config::core::get "_ports" "ports_${_name}"
    config::core::get "_vlan" "vlan_${_name}"
    config::core::get "_addr" "addr_${_name}"
    config::core::get "_mtu" "mtu_${_name}"
    config::core::get "_priv" "private_${_name}" "no"

    printf "${_format}" "${_name}" "standard" "${_id:--}" "${_addr:--}" "${_priv}" "${_mtu:--}" \
      "${_vlan:--}" "${_ports:--}"
}

# create a standard virtual switch
#
switch::standard::create(){

    # store configuration
    config::core::set "switch_list" "${_switch}" "1"
    config::core::set "type_${_switch}" "standard"

    [ -n "${_if}" ] && config::core::set "ports_${_switch}" "${_if}"
    [ -n "${_vlan}" ] && config::core::set "vlan_${_switch}" "${_vlan}"
    [ -n "${_addr}" ] && config::core::set "addr_${_switch}" "${_addr}"
    [ -n "${_priv}" ] && config::core::set "private_${_switch}" "${_priv}"
    [ -n "${_mtu}" ] && config::core::set "mtu_${_switch}" "${_mtu}"

    config::core::load
    switch::standard::init "${_switch}"
}

# destroy a standard switch
#
# @param string _switch name of the switch to destroy
#
switch::standard::remove(){
    local _switch="$1"
    local _id

    # get the bridge id
    switch::standard::id "_id" "${_switch}"
    [ $? -eq 0 ] || return 1

    # remove all member interfaces
    switch::standard::__remove_members "${_switch}" "${_id}"

    # destroy the bridge
    ifconfig "${_id}" destroy >/dev/null 2>&1
}

# add a new interface to this switch
#
# @param string _switch name of the switch
# @param string _if the interface to add
#
switch::standard::add_member(){
    local _switch="$1"
    local _if="$2"
    local _id _vlan _mtu
    
    switch::standard::id "_id" "${_switch}" || util::err "unable to locate switch id"
    config::core::get "_vlan" "vlan_${_switch}"
    config::core::get "_mtu" "mtu_${_switch}"
    switch::standard::__configure_port "${_switch}" "${_id}" "${_if}" "${_vlan}" "${_mtu}"
    config::core::set "ports_${_switch}" "${_if}" "1"
}

# remove a member interface from this switch
#
# @param string _switch name of the switch
# @param string _if the interface to remove
#
switch::standard::remove_member(){
    local _switch="$1"
    local _if="$2"
    local _id _vlan

    switch::standard::id "_id" "${_switch}" || util::err "unable to locate switch id"
    config::core::remove "ports_${_switch}" "${_if}"
    config::core::get "_vlan" "vlan_${_switch}"
    switch::standard::__unconfigure_port "${_switch}" "${_id}" "${_if}" "${_vlan}"
}

# set vlan id
#
# @param string _switch name of switch
# @param int _vlan vlan id to set
#
switch::standard::vlan(){
    local _switch="$1"
    local _vlan="$2"
    local _id

    switch::standard::id "_id" "${_switch}" || util::err "unable to locate switch id"
    switch::standard::__remove_members "${_switch}" "${_id}"

    # update configuration
    if [ "${_vlan}" = "0" ]; then
        config::core::remove "vlan_${_switch}"
    else
        config::core::set "vlan_${_switch}" "${_vlan}"
    fi

    config::core::load
    switch::standard::__add_members "${_switch}" "${_id}"
}

# set or remove ip address
#
# @param string _swtich name of the switch
# @param string _addr address or "none"
# @scope _id switch if from parent switch::address
#
switch::standard::address(){
    local _switch="$1"
    local _addr="$2"
    local _curr

    if [ "${_addr}" = "none" ]; then

        config::core::get "_curr" "addr_${_switch}"
        [ $? -eq 0 ] || util::err "unable to locate an existing address for this switch"

        config::core::remove "addr_${_switch}"
        ifconfig "${_id}" "${_curr}" delete
    else

        config::core::set "addr_${_switch}" "${_addr}"
        ifconfig "${_id}" "${_addr}"
    fi
}

# add all member interfaces to a switch
#
# @param string _switch the name of the switch
# @param string _id interface id for the switch
#
switch::standard::__add_members(){
    local _switch="$1"
    local _id="$2"
    local _ports _vlan _port _mtu

    # get the id if not provided
    if [ -z "${_id}" ]; then
        switch::standard::id "_id" "${_switch}" || util:err "failed to get switch id while adding members"
    fi

    config::core::get "_ports" "ports_${_switch}"
    config::core::get "_vlan" "vlan_${_switch}"
    config::core::get "_mtu" "mtu_${_switch}"

    if [ -n "${_ports}" ]; then
        for _port in ${_ports}; do
            switch::standard::__configure_port "${_switch}" "${_id}" "${_port}" "${_vlan}" "${_mtu}"
        done
    fi
}

# remove member interfaces from a switch
#
# @param string _switch the name of the switch
# @param string _id bridge id if already known
#
switch::standard::__remove_members(){
    local _switch="$1"
    local _id="$2"
    local _ports _port _vlan

    # get id if not given to us
    if [ -z "${_id}" ]; then
        switch::standard::id "_id" "${_switch}"
        [ $? -eq 0 ] || util::err "failed to get switch id while removing members"
    fi

    # get full port list
    config::core::get "_ports" "ports_${_switch}"
    config::core::get "_vlan" "vlan_${_switch}"

    if [ -n "${_ports}" ]; then
        for _port in ${_ports}; do
           switch::standard::__unconfigure_port "${_switch}" "${_id}" "${_port}" "${_vlan}"
        done
    fi
}

# configure a local port for our bridge
#
# @param string _switch the switch to add port to
# @param string _id the bridge id of the switch
# @param string _port the interface to add 
# @param int _vlan vlan number if assigned to this switch
# @param int _mtu custom mtu to use for this port
#
switch::standard::__configure_port(){
    local _switch="$1"
    local _id="$2"
    local _port="$3"
    local _vlan="$4"
    local _mtu="$5"
    local _viface _vname

    # try to set mtu of port?
    [ -n "${_mtu}" ] && ifconfig "${_port}" mtu ${_mtu} >/dev/null 2>&1

    # vlan enabled?
    if [ -n "${_vlan}" ]; then

        # see if vlan interface already exists
        _vname="${_port}.${_vlan}"
        switch::standard::id "_viface" "${_vname}"

        # create if needed
        if [ $? -ne 0 ]; then
            # use our id as the interface name here.
            # it should always be a valid name and interface.vlan-id is much easier to understand in ifconfig
            # than a bunch of vlanX interfaces
            _viface=$(ifconfig vlan create vlandev "${_port}" vlan "${_vlan}" descr "vm-vlan/${_switch}/${_vname}" name "${_vname}" group vm-vlan up 2>/dev/null)
            [ $? -eq 0 ] || util::err "failed to create vlan interface for port ${_port} on switch ${_switch}"
        fi

        switch::set_viid "${_vname}" "${_viface}"
        ifconfig ${_id} addm ${_viface} >/dev/null 2>&1
    else
        # add to bridge, nice and simple :)
        ifconfig ${_id} addm ${_port} >/dev/null 2>&1
    fi

    [ $? -eq 0 ] || util::err "failed to add member ${_port} to the virtual switch ${_switch}"
}

# unconfigure a local port
#
# @param string _switch the switch to remove port from
# @param string _id the bridge id of the switch
# @param string _port the interface to remove
# @param string _vlan vlan number if assigned to this switch
#
switch::standard::__unconfigure_port(){
    local _switch="$1"
    local _id="$2"
    local _port="$3"
    local _vlan="$4"
    local _vid

    if [ -n "${_vlan}" ]; then
        # get vlan interface
        switch::standard::id "_vid" "${_port}.${_vlan}"

        # remove the vlan interface, it will be removed from bridge automatically
        [ $? -eq 0 ] && ifconfig ${_vid} destroy >/dev/null 2>&1
    else
        ifconfig ${_id} deletem ${_port} >/dev/null 2>&1
    fi
}

# get the id for a standard switch
# this returns the associated bridge name
#
# @param string _var variable to put id into
# @param string _switch the switch to look for
# @return 0 on success
#
switch::standard::id(){
    switch::find "$1" "$2"
}

# creates a standard tap interface for a guest
# relies heavily on variables set in the main vm::run function
#
# @modifies _func _devices
# @return 1 if we don't get a tap device
#
switch::standard::provision(){
    local _tap _custom_tap _sid _mtu _member_type _iname

    config::get "_custom_tap" "network${_num}_device"
    config::get "_iname" "network${_num}_name"

    # create interface
    if [ -n "${_custom_tap}" ]; then
        _tap="${_custom_tap}"
    elif [ -n "${_iname}" ]; then
        _tap=$(ifconfig tap create name "${_iname}")
    else
        _tap=$(ifconfig tap create)
    fi

    [ -z "${_tap}" ] && return 1;

    util::log "guest" "${_name}" "initialising network device ${_tap}"
    ifconfig "${_tap}" descr "vmnet/${_name}/${_num}/${_switch:-custom}" group vm-port >/dev/null 2>&1

    if [ -n "${_switch}" ]; then
        switch::id "_sid" "${_switch}"

        # should this be a span member?
        _member_type="addm"
        config::yesno "network${_num}_span" && _member_type="span"

        if [ -n "${_sid}" ]; then
            _mtu=$(ifconfig "${_sid}" | head -n1 | awk '{print $NF}')

            if [ "${_mtu}" != "1500" ]; then
                util::log "guest" "${_name}" "setting mtu of ${_tap} to ${_mtu}"
                ifconfig "${_tap}" mtu "${_mtu}" >/dev/null 2>&1
            fi

            util::log "guest" "${_name}" "adding ${_tap} -> ${_sid} (${_switch} ${_member_type})"
            ifconfig "${_sid}" "${_member_type}" "${_tap}" >/dev/null 2>&1 || util::log "guest" "${_name}" "failed to add ${_tap} to ${_sid}"

            util::log "guest" "${_name}" "bring up ${_tap} -> ${_sid} (${_switch} ${_member_type})"
            ifconfig "${_tap}" up >/dev/null 2>&1 || util::log "guest" "${_name}" "failed to bring up ${_tap} in ${_sid}"

            # set private if configured
            switch::is_private "${_switch}" && ifconfig "${_sid}" "private" "${_tap}" >/dev/null 2>&1
        else
            util::log "guest" "${_name}" "failed to find virtual switch '${_switch}'"
        fi
    fi

    _devices="${_devices} -s ${_bus}:${_slot}:${_func},${_emulation},${_tap}"
    [ -n "${_mac}" ] && _devices="${_devices},mac=${_mac}"

    _func=$((_func + 1))
    [ -z "${_custom_tap}" ] && _taplist="${_taplist}${_taplist:+ }${_tap}"
}
